/*
* Copyright 2010 Pavel Kozhin 
*
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed  under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License.
*
* Author: Pavel Kozhin.
*/

package org.bookmarksframework.core
{
	import flash.display.InteractiveObject;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.setTimeout;
	
	import mx.collections.ArrayList;
	import mx.collections.IList;
	import mx.core.IFactory;
	import mx.core.IFlexDisplayObject;
	import mx.core.IInvalidating;
	import mx.core.Singleton;
	import mx.events.FlexEvent;
	
	import org.bookmarksframework.events.FragmentChangeEvent;
	import org.bookmarksframework.events.FragmentEvent;
	import org.bookmarksframework.manager.IBookmarksManager;
	import org.bookmarksframework.rule.IRuleParser;
	import org.bookmarksframework.target.FragmentTarget;
	import org.bookmarksframework.target.IFragmentTarget;
	import org.bookmarksframework.util.PropertyBinder;
	
	[Event(name="change", type="org.bookmarksframework.events.FragmentChangeEvent")]
	
	[Event(name="register", type="org.bookmarksframework.events.FragmentEvent")]
	
	[Event(name="unregister", type="org.bookmarksframework.events.FragmentEvent")]
	
	[Event(name="updating", type="org.bookmarksframework.events.FragmentEvent")]
	
	[Event(name="update", type="org.bookmarksframework.events.FragmentEvent")]
	
	[Event(name="committing", type="org.bookmarksframework.events.FragmentCommitEvent")]
	
	[Event(name="postpone", type="org.bookmarksframework.events.FragmentEvent")]
	
	[Event(name="resume", type="org.bookmarksframework.events.FragmentEvent")]
	
	[Event(name="commit", type="org.bookmarksframework.events.FragmentCommitEvent")]
	
	[Event(name="commitComplete", type="org.bookmarksframework.events.FragmentEvent")]
	
	public class Fragment extends EventDispatcher implements IFragment
	{
		protected var manager:IBookmarksManager;
		
		protected var binding:PropertyBinder = null;
		
		protected var relationsController:RelationsController;
		
		private var waitingTarget:Boolean = true;
		
		private var initHandled:Boolean = false;
		
		protected var setupToDestroy:Boolean = false;
		
		public function initialize(host:IFlexDisplayObject):void
		{
			if (!host)
				throw new Error("Bookmarks Error: Host can't be null.");
			
			if (registered)
				return;
			
			_host = host;
			
			if (hasEventListener("fragmentHostChanged"))
				dispatchEvent(new Event("fragmentHostChanged"));
			
			host.addEventListener(FlexEvent.PREINITIALIZE, initHandler, false, -1000);
		}
		
		private var cachedParent:IFragment;
		private var _parent:IFragment;
		
		public function get parent():IFragment
		{
			return _parent || cachedParent || 
				(cachedParent = relationsController.execute());
		}

		public function set parent(value:IFragment):void
		{
			if (_parent != value && cachedParent != value)
			{
				_parent = value;
				
				if (registered)
					manager.unregister(this);
				
				manager.register(this);
			}
		}

		private var explicitActive:Boolean = false;
		
		private var _active:Boolean;
		
		public function get active():Boolean
		{
			var result:Boolean = false;
			
			if (explicitActive)
			{
				result = _active;
			}
			else if (parent &&  
				(parent.isChildActiveFunction != null))
			{
				result = parent.isChildActiveFunction(this);
			}
			else
			{
				result = (host.parent && host.visible && 
					host.parent.visible) && ((parent) ? parent.active : true);
			}
			return result;
		}
		
		public function set active(value:Boolean):void
		{
			_active = value;
			explicitActive = true;
		}
		
		private var _host:IFlexDisplayObject;
		
		[Bindable("fragmentHostChanged")]
		public function get host():IFlexDisplayObject
		{
			return _host;
		}
		
		private var _uid:String;
		
		public function get uid():String
		{
			return _uid;
		}
		
		public function set uid(value:String):void
		{
			_uid = value;
		}
		
		private var _id:String;
		
		public function get id():String
		{
			return _id;
		}
		
		public function set id(value:String):void
		{
			_id = value;
		}
		
		private var _registered:Boolean = false;
		
		public function get registered():Boolean
		{
			return _registered;
		}
		
		public function set registered(value:Boolean):void
		{
			_registered = value;
			
			if (!value)
				cachedParent = null;
			
			var evtType:String = value ? 
				FragmentEvent.REGISTER : FragmentEvent.UNREGISTER;
			
			if (hasEventListener(evtType))
			{
				var evt:FragmentEvent = new FragmentEvent(evtType, this);
				dispatchEvent(evt);
			}
			
			if (target is IFragmentTarget)
			{
				targetField = "data";
				IFragmentTarget(target).fragment = (value) ? this : null;
			}
			
			if (value)
				bind();
			
			if (setupToDestroy)
				manager.destroy(this);
		}
		
		private var _enabled:Boolean = true;
		
		public function get enabled():Boolean
		{
			return _enabled;
		}
		
		public function set enabled(value:Boolean):void
		{
			_enabled = value;
		}
		
		private var _rule:Object = null;
		
		public function get rule():Object
		{
			return _rule;
		}
		
		public function set rule(value:Object):void
		{
			if (_rule == value)
				return;
			
			_rule = value;
		}
		
		private var _targetField:String;
		
		[Bindable]
		public function get targetField():String
		{
			return _targetField;
		}
		
		public function set targetField(value:String):void
		{
			if (_targetField == value)
				return;
			
			if (target && !(value in target))
				throw new Error("Bookmarks Error: Target doesn't have " +
					"property with name \""+value+"\".");
			
			_targetField = value;
			
			if (!registered)
				return;
			
			bind();
		}
		
		private var _target:Object;
		
		[Bindable]
		public function get target():Object
		{
			return _target;
		}
		
		public function set target(value:Object):void
		{
			if (_target == value)
				return;
			
			if (!(value is IEventDispatcher))
				throw new Error("Bookmarks Error: Target should implement IEventDispatcher.");
			
			if (targetField && !(targetField in value))
				throw new Error("Bookmarks Error: Target doesn't have property with " +
					"name \""+targetField+"\".");
			
			if (_target is IFragmentTarget && IFragmentTarget(_target).fragment)
				IFragmentTarget(_target).fragment = null;
			
			if (blockable.getItemIndex(_target) != -1)
				blockable.removeItemAt(blockable.getItemIndex(_target));
			
			_target = value;
			
			if (value is InteractiveObject)
				blockable.addItemAt(value, 0);
			
			if (value && waitingTarget)
			{
				init();
				waitingTarget = false;
			}
			else if (!waitingTarget)
			{
				if ((value is IFragmentTarget) && registered)
				{
					if (targetField != "data") targetField = "data";
					IFragmentTarget(value).fragment = this;
				}
				relationsController.init();
			}
			
			bind();
		}	
		
		private var _value:* = null;
		
		[Bindable]
		public function get value():*
		{
			return (binding) ? binding.value: _value;
		}
		
		public function set value(value:*):void
		{
			if (manager.isBlocked && _value != value)
				return;
			
			if (binding)
			{
				binding.value = value;
				
				if (target is IInvalidating)
					IInvalidating(target).validateNow();
			}
			else
			{
				_value = value;
			}
			
			if (value !== null && !_part)
				refreshPart();
		}
		
		private var _ruleParserFactory:IFactory = null;
		
		public function get ruleParserFactory():IFactory
		{
			return _ruleParserFactory;
		}
		
		public function set ruleParserFactory(value:IFactory):void
		{
			if (!value)
				return;
			
			_ruleParserFactory = value;
			_ruleParser = value.newInstance() as IRuleParser;
		}
		
		protected var _ruleParser:IRuleParser = null;
		
		public function get ruleParser():IRuleParser
		{
			_ruleParser.rule = rule;
			return _ruleParser;
		}
		
		private var _path:Path = null;
		
		[Bindable("fragmentPathChanged")]
		public function get path():Path
		{
			if (manager)
			{
				var p:Path = manager.activePath || 
					manager.defaultPath;
				if (p)
				{
					_path = p.getFragment(this);
				}
			}
			
			return _path;
		}
		
		protected var parts:Dictionary = new Dictionary();
		protected var _part:* = null;
		
		[Bindable("fragmentPartChanged")]
		public function get part():*
		{
			return _part;
		}
		
		public function set part(value:*):void
		{
			if (this.value === null || value === _part)
				return;
			
			if (ruleParser.findValue(value + "") === this.value)
			{
				parts[this.value] = value; 
				_part = value;
				if (hasEventListener("fragmentPartChanged"))
					dispatchEvent(new Event("fragmentPartChanged"));
			}
		}
		
		private var _substitution:* = null;
		
		public function get substitution():*
		{
			return _substitution;
		}
		
		public function set substitution(value:*):void
		{
			_substitution = value;
		}
		
		private var _pending:Boolean = false;
		
		[Bindable]
		public function get pending():Boolean
		{
			return _pending;
		}
		
		public function set pending(value:Boolean):void
		{
			if (_pending == value)
				return;
			
			_pending = value;
			
			var type:String = (value) ? FragmentEvent.POSTPONE :
				FragmentEvent.RESUME;
			
			dispatchEvent(new FragmentEvent(type, this));
		}
		
		public function commit(path:*):void
		{
			if (registered)
			{
				manager.commit(path, this);
			}
		}
		
		public function destroy():void
		{
			if (setupToDestroy)
				return;
			
			setupToDestroy = true;
			
			if (manager)
				manager.unregister(this);
			
			if (binding)
				binding.unbind();
			
			if (host)
				host.removeEventListener(FlexEvent.PREINITIALIZE, initHandler);
			
			if (relationsController)
				relationsController.destroy();
			
			if (target is IFragmentTarget && IFragmentTarget(target).fragment)
				IFragmentTarget(target).fragment = null;
			
			binding = null;
			manager = null;
			_host = null;
			_ruleParser = null;
			_ruleParser = null;
			relationsController = null;
		}
		
		private var _isChildActiveFunction:Function = null;
		
		[Bindable]
		public function get isChildActiveFunction():Function
		{
			return _isChildActiveFunction;
		}

		public function set isChildActiveFunction(value:Function):void
		{
			_isChildActiveFunction = value;
		}
		
		private var _relations:IList = new ArrayList();
		
		public function get relations():IList
		{
			return _relations;
		}
		
		private var _blockable:IList = new ArrayList();
		
		public function get blockable():IList
		{
			return _blockable;
		}
		
		protected function init():void
		{
			if (!initHandled)
				return;
			
			if (!target)
			{
				waitingTarget = true;
				return;
			}
			
			try {
				manager = 
					IBookmarksManager(Singleton.getInstance("org.bookmarksframework." +
						"manager::IBookmarksManager"));
			} catch (e:Error) {
				throw new Error("Bookmarks Error: Please, check that IBookmarksManager " +
					"instantiation happens at the very beginning and before the first " +
					"IFragment initialization. Also make sure that IBookmarksManager is " +
					"properly registered via mx.core.Singleton");
			}
			
			addEventListener(FragmentEvent.COMMIT_COMPLETE, 
				commitCompleteHandler, false, Number.MAX_VALUE);
			
			relationsController = new RelationsController(this);
			relationsController.init();
			manager.register(this);
		}
		
		protected function bind():void
		{
			if (target && targetField)
			{
				if (binding)
					binding.unbind();
				
				binding = new PropertyBinder(target, targetField, bindingUpdated);
				binding.ignoreFirstBinding = true;
				
				binding.bind();
				refreshPart();
				
				if (_value)
					value = _value;
			}
		}
		
		protected function unbind():void
		{
			if (binding)
				binding.unbind();
		}
		
		protected function bindingUpdated(oldValue:Object, newValue:Object):void
		{
			if (!registered)
				return;
			
			if (manager.isBlocked && oldValue != newValue)
			{
				binding.value = oldValue;
				return;
			}
			
			var evt:FragmentChangeEvent;
			var oldPart:* = part;
			
			_value = newValue;
			refreshPart();
			
			if ((newValue !== null) && (oldValue != -1))
			{
				evt = new FragmentChangeEvent(this, oldValue, 
					newValue, oldPart, part);
				dispatchEvent(evt);
			}
		}
		
		protected function refreshPart():void
		{
			if ((value === null) || !ruleParser || !rule)
				return;
			
			var oldPath:* = _part;
			var newPath:*;
			
			if (parts[value])
				newPath = parts[value];
			else if (substitution !== null)
				newPath = ruleParser.substituteValue(ruleParser.findKey(value+""), substitution); 
			else
				newPath = ruleParser.findKey(value + "");
			
			if (oldPath != newPath)
			{
				_part = newPath;
				if (hasEventListener("fragmentPartChanged"))
					dispatchEvent(new Event("fragmentPartChanged"));
			}
		}
		
		protected function commitCompleteHandler(event:FragmentEvent):void
		{
			if (hasEventListener("fragmentPathChanged"))
				dispatchEvent(new Event("fragmentPathChanged"));
		}
		
		private function initHandler(event:FlexEvent):void
		{
			initHandled = true;
			event.target.removeEventListener(FlexEvent.PREINITIALIZE, initHandler);
			init();
		}
	}
}